package com.bitcointoolkit.web.service.tx;

import com.bitcointoolkit.common.cash.CipherUtils;
import com.bitcointoolkit.common.cash.EncodeUtil;
import com.google.common.collect.Maps;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * [类描述]
 *
 * @author caican
 * @date 18/6/17
 */
public class DecodedTx {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    private String txid;
    private String hash;
    private long size;//TODO
    private long version;
    private long locktime;

    private List<TxIn> vin;
    private List<TxOut> vout;

    public void decode(String rawhex) {
        int[] rawdata = EncodeUtil.toInts(rawhex);
        if (rawdata.length < 63) {
            throw new RuntimeException("rawdata lenth too short!");
        }
        version = EncodeUtil.parseInt32(EncodeUtil.subInt(rawdata, 0, 4));
        logger.debug("version:"+ version);

        int vinSize = rawdata[4];
        logger.debug("vin size:" + vinSize);
        if (vinSize == 0) {//表明是bitcoin tx 扩展格式，隔离见证用的，目前不支持
            throw new IllegalArgumentException("不支持bitcoin tx 扩展格式");
        }
        vin = new ArrayList<>(vinSize);
        int currentPos = 5;
        for (int i=0;i<vinSize;i++) {
            TxIn txIn = decodeVin(rawdata, currentPos);
            vin.add(txIn);
            currentPos += txIn.countSize();
        }

        int voutSize = rawdata[currentPos];
        currentPos += 1;
        logger.debug("vout size:" + voutSize);
        if (voutSize == 0) {
            throw new IllegalArgumentException("voutSize == 0");
        }
        vout = new ArrayList<>();
        for (int i=0;i<voutSize;i++) {
            TxOut txOut = decodeVout(rawdata, currentPos);
            txOut.setN(i);
            vout.add(txOut);
            currentPos += txOut.countSize();
        }

        locktime = EncodeUtil.parseInt32(EncodeUtil.subInt(rawdata, currentPos, currentPos+4));
        logger.debug("locktime:" + locktime);

        txid = countTxId(rawhex);
        hash = txid;
        size = rawdata.length;
    }

    private String countTxId(String input) {
        byte[] bbb = EncodeUtil.toBytes(input);
        byte[] ccc = CipherUtils.doubleSHA256(bbb);
        return EncodeUtil.parseBigEndianToString(ccc);
    }

    private TxOut decodeVout(int[] input, int startIdx) {
        TxOut txOut = new TxOut();
        int currentIdx = startIdx;
        txOut.setValue(EncodeUtil.parseInt64(
                EncodeUtil.subInt(input, currentIdx, currentIdx + 8)
        ));
        currentIdx += 8;
        logger.debug("value:" + txOut.getValue());
        Map.Entry<Integer,Integer> scLenRet = getScriptLen(input, currentIdx);
        int scriptLen = scLenRet.getKey();
        currentIdx += scLenRet.getValue();
        logger.debug("script length:" + scriptLen);
        txOut.setTxScript(EncodeUtil.parseToString(
                EncodeUtil.subInt(input, currentIdx, currentIdx+scriptLen)
        ));
        currentIdx += scriptLen;
        logger.debug("script:" + txOut.getScriptHex());

        Assert.assertEquals((currentIdx - startIdx), txOut.countSize());
        return txOut;
    }

    /**
     * 脚本长度 小于等于 252字节时，用第一个字节表示长度。
     * 脚本长度 大于 252字节时，第一个字节固定为 0xfd 后面两个字节为长度，大端字节序
     * @return [脚本长度, 脚本长度标示的字节数]
     */
    private Map.Entry<Integer,Integer> getScriptLen(int[] input, int startIdx) {
        int scriptLen, scLenLen;
        if (input[startIdx] < 0xfd) {
            scriptLen = input[startIdx];
            scLenLen = 1;
        } else {// TODO 还有4字节的情况没做
            scriptLen = EncodeUtil.parseInt16(EncodeUtil.subInt(input, startIdx+1, startIdx+3));
            scLenLen = 3;
        }

        return Maps.immutableEntry(scriptLen, scLenLen);
    }


    private TxIn decodeVin(int[] input, int startIdx){
        TxIn txIn = new TxIn();
        int currentIdx = startIdx;
        String txId = EncodeUtil.parseBigEndianToString(
                EncodeUtil.subInt(input, currentIdx, currentIdx + 32));
        currentIdx += 32;

        OutPoint outPoint = new OutPoint();
        outPoint.setTxid(txId);
        logger.debug("txId:"+txId);
        outPoint.setN( (int) EncodeUtil.parseInt32(
            EncodeUtil.subInt(input, currentIdx, currentIdx+4)));
        currentIdx += 4;
        logger.debug("n:" + outPoint.getN());

        Map.Entry<Integer,Integer> scLenRet = getScriptLen(input, currentIdx);
        int scriptLen = scLenRet.getKey();
        currentIdx += scLenRet.getValue();
        logger.debug("script length:" + scriptLen);

        txIn.setPrevout(outPoint);
        txIn.setTxScript(EncodeUtil.parseToString(
                EncodeUtil.subInt(input, currentIdx, currentIdx+scriptLen)
        ));
        currentIdx += scriptLen;
        logger.debug("script:" + txIn.getScriptHex());

        txIn.setSequence(EncodeUtil.parseInt32(
                EncodeUtil.subInt(input, currentIdx, currentIdx+4)));
        logger.debug("Sequence:" + txIn.getSequence());
        currentIdx +=4;

        Assert.assertEquals((currentIdx - startIdx), txIn.countSize());
        return txIn;
    }


    public String getTxid() {
        return txid;
    }

    public String getHash() {
        return hash;
    }

    public long getSize() {
        return size;
    }

    public long getVersion() {
        return version;
    }

    public long getLocktime() {
        return locktime;
    }

    public List<TxIn> getVin() {
        return vin;
    }

    public List<TxOut> getVout() {
        return vout;
    }
}
